package common.db.dao.hibernate;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.stream.Collectors;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ClassFile;
import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.StringMemberValue;

import javax.persistence.EmbeddedId;
import javax.persistence.Table;

import org.apache.logging.log4j.LogManager;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import common.db.dao.DaoUtil;

import core.config.Factory;
import core.exception.PhysicalException;
import core.util.bean.CamelCase;

/**
 * エンティティ共通クラス
 *
 * @author Tadashi Nakayama
 */
public final class EntityUtil {

	/**
	 * コンストラクタ
	 */
	private EntityUtil() {
		throw new AssertionError();
	}

	/**
	 * 動的作成確認
	 * @param cls テーブルクラス
	 * @return 動的作成の場合 true を返す。
	 */
	public static boolean isDynamicInsert(final Class<?> cls) {
		DynamicInsert ent = cls.getAnnotation(DynamicInsert.class);
		return ent != null && ent.value();
	}

	/**
	 * 動的更新確認
	 * @param cls テーブルクラス
	 * @return 動的更新の場合 true を返す。
	 */
	public static boolean isDynamicUpdate(final Class<?> cls) {
		DynamicUpdate ent = cls.getAnnotation(DynamicUpdate.class);
		return ent != null && ent.value();
	}

	/**
	 * エンティティクラス拡張
	 * @param <T> ジェネリクス
	 * @param cls 基底クラス
	 * @param table テーブル名
	 * @return 拡張クラス
	 */
	public static <T extends Serializable> Class<T> extend(
					final Class<T> cls, final String table) {
		String name = cls.getPackage().getName() + "." + CamelCase.convert(table);

		Class<T> c = Factory.loadClass(name);
		if (c != null) {
			return c;
		}

		try {
			ClassPool pool = ClassPool.getDefault();
			CtClass ct = pool.makeClass(name, pool.get(cls.getName()));

			ClassFile cf = ct.getClassFile2();
			AnnotationsAttribute attr = new AnnotationsAttribute(
							cf.getConstPool(), AnnotationsAttribute.visibleTag);
			attr.addAnnotation(getJpaTable(cf, table));
			if (isDynamicInsert(cls)) {
				attr.addAnnotation(new Annotation(
								DynamicInsert.class.getName(), cf.getConstPool()));
			}
			if (isDynamicUpdate(cls)) {
				attr.addAnnotation(new Annotation(
								DynamicUpdate.class.getName(), cf.getConstPool()));
			}
			ct.setAttribute(AnnotationsAttribute.visibleTag, attr.get());

			return ct.toClass(cls.getClassLoader(), null).asSubclass(Serializable.class);

		} catch (final NotFoundException | CannotCompileException e) {
			LogManager.getLogger().error(e.getMessage(), e);
			throw new PhysicalException(e);
		}
	}

	/**
	 * JPA Tableアノテーション取得
	 * @param cf ClassFile
	 * @param table テーブル名
	 * @return アノテーション
	 */
	private static Annotation getJpaTable(final ClassFile cf, final String table) {
		Annotation anno = new Annotation(Table.class.getName(), cf.getConstPool());
		anno.addMemberValue("name", new StringMemberValue(table, cf.getConstPool()));
		return anno;
	}

	/**
	 * 作成SQL化
	 * @param obj オブジェクト
	 * @param param パラメタ
	 * @return SQL
	 */
	public static String toInsertSql(final Serializable obj, final List<Object> param) {
		param.clear();

		StringJoiner column = new StringJoiner(", ");
		StringJoiner value = new StringJoiner(", ");
		toInsertType(obj, param, column, value, isDynamicInsert(obj.getClass()));
		return "INSERT INTO " + DaoUtil.getTableName(obj.getClass())
				+ "(" + column.toString() + ") VALUES(" + value.toString() + ")";
	}

	/**
	 * Insert形式SQL化
	 * @param obj オブジェクト
	 * @param param パラメタ
	 * @param column カラム名バッファ
	 * @param value 値バッファ
	 * @param dynamic 動的フラグ
	 */
	private static void toInsertType(final Object obj, final List<Object> param,
					final StringJoiner column, final StringJoiner value, final boolean dynamic) {

		Method[] methods = obj.getClass().getMethods();
		Arrays.sort(methods, Comparator.comparing(Method::getName));

		for (final Method m : methods) {
			if (Factory.isGetter(m)) {
				Object o = Factory.invoke(obj, m);
				if (DaoUtil.isId(m)) {
					if (m.getAnnotation(EmbeddedId.class) != null) {
						toInsertType(o, param, column, value, false);
						continue;
					}
				}
				if (o != null || !dynamic) {
					param.add(o);
					column.add(DaoUtil.getColumnName(m));
					value.add(":" + param.size());
				}
			}
		}
	}

	/**
	 * 更新SQL化
	 * @param obj オブジェクト
	 * @param param パラメタ
	 * @return SQL
	 */
	public static String toUpdateSql(final Serializable obj, final List<Object> param) {
		param.clear();

		StringJoiner sb = new StringJoiner(", ");
		Method m = toUpdateType(obj, param, sb, isDynamicUpdate(obj.getClass()));
		String where = toWhereString(Factory.invoke(obj, m), param, m);
		return "UPDATE " + DaoUtil.getTableName(obj.getClass()) + " SET " + sb.toString() + where;
	}

	/**
	 * Update形式SQL化
	 * @param obj オブジェクト
	 * @param param パラメタ
	 * @param sb バッファ
	 * @param dynamic 動的フラグ
	 * @return idメソッド
	 */
	private static Method toUpdateType(final Object obj, final List<Object> param,
					final StringJoiner sb, final boolean dynamic) {

		Method[] methods = obj.getClass().getMethods();
		Arrays.sort(methods, Comparator.comparing(Method::getName));

		Method id = null;
		for (final Method m : methods) {
			if (Factory.isGetter(m)) {
				if (DaoUtil.isId(m)) {
					id = m;
				} else {
					Object o = Factory.invoke(obj, m);
					if (o != null || !dynamic) {
						param.add(o);
						sb.add(DaoUtil.getColumnName(m) + " = :" + param.size());
					}
				}
			}
		}
		return id;
	}

	/**
	 * 削除SQL化
	 * @param obj オブジェクト
	 * @param param パラメタ
	 * @return SQL
	 */
	public static String toDeleteSql(final Serializable obj, final List<Object> param) {
		param.clear();

		Optional<Method> opt = DaoUtil.getIdMethod(obj.getClass());
		Method m = opt.orElse(null);
		Object o = Factory.invoke(obj, m);
		return "DELETE FROM " + DaoUtil.getTableName(obj.getClass()) + toWhereString(o, param, m);
	}

	/**
	 * Where句作成
	 * @param o オブジェクト
	 * @param param パラメタ
	 * @param m Idメソッド
	 * @return Where句
	 */
	public static String toWhereString(final Object o, final List<Object> param, final Method m) {
		if (m != null) {
			StringJoiner where = new StringJoiner(", ");
			if (m.getAnnotation(EmbeddedId.class) != null) {
				toUpdateType(o, param, where, false);
			} else {
				param.add(o);
				where.add(DaoUtil.getColumnName(m) + " = :" + param.size());
			}
			return " WHERE " + where.toString();
		}
		return "";
	}

	/**
	 * エンティティ化
	 * @param <T> ジェネリクス
	 * @param data データ
	 * @param cls クラス
	 * @return エンティティ
	 */
	public static <T> T toEntity(final Map<String, Object> data, final Class<T> cls) {
		return toInstance(data, cls);
	}

	/**
	 * エンティティリスト化
	 * @param <T> ジェネリクス
	 * @param data データ
	 * @param cls クラス
	 * @return エンティティリスト
	 */
	public static <T> List<T> toEntityList(
					final List<Map<String, Object>> data, final Class<T> cls) {
		return data.stream().map(m -> toInstance(m, cls)).collect(Collectors.toList());
	}

	/**
	 * インスタンス化
	 * @param <T> ジェネリクス
	 * @param data データ
	 * @param cls クラス
	 * @return インスタンス
	 */
	private static <T> T toInstance(final Map<String, Object> data, final Class<T> cls) {
		try {

			T ret = cls.newInstance();
			for (final Method m : cls.getMethods()) {
				if (Factory.isGetter(m)) {
					Object obj = data.get(DaoUtil.getColumnName(m));
					if (DaoUtil.isId(m)) {
						if (m.getAnnotation(EmbeddedId.class) != null) {
							obj = toInstance(data, m.getReturnType());
						}
					}

					Method mt = Factory.getMethod(
									cls, "set" + Factory.toItemName(m), m.getReturnType());
					if (obj != null && mt != null) {
						Factory.invoke(ret, mt, toValidType(obj, mt.getParameterTypes()[0]));
					}
				}
			}
			return ret;

		} catch (final InstantiationException | IllegalAccessException e) {
			LogManager.getLogger().error(e.getMessage(), e);
			throw new PhysicalException(e);
		}
	}

	/**
	 * 型変換
	 * @param obj オブジェクト
	 * @param cls クラス
	 * @return 変換後オブジェクト
	 */
	private static Object toValidType(final Object obj, final Class<?> cls) {
		if (!cls.equals(obj.getClass())) {
			if (Number.class.isInstance(obj)) {
				Constructor<?> c = getConstructor(cls, String.class);
				if (c != null) {
					return Factory.construct(c, obj.toString());
				}
			}
		}
		return obj;
	}

	/**
	 * コンストラクタ取得
	 * @param cls クラス
	 * @param arg コンストラクタ引数クラス
	 * @return コンストラクタ
	 */
	private static Constructor<?> getConstructor(final Class<?> cls, final Class<?>... arg) {
		try {

			if (cls.isPrimitive()) {
				return Factory.toReference(cls).getConstructor(arg);
			} else if (Number.class.isAssignableFrom(cls)) {
				return cls.getConstructor(String.class);
			}
			return null;

		} catch (final NoSuchMethodException e) {
			LogManager.getLogger().error(e.getMessage(), e);
			throw new PhysicalException(e);
		}
	}
}
